﻿#region (C) Koninklijke Philips Electronics N.V. 2011
//
// All rights are reserved. Reproduction or transmission in whole or in part, in
// any form or by any means, electronic, mechanical or otherwise, is prohibited 
// without the prior written consent of the copyright owner.
// 
// Filename: ModelBase.cs
//
#endregion

using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;

namespace TestRunner {

    /// <summary>
    /// Base class for implementations of MVVM active models.
    /// </summary>
    /// <seealso cref="INotifyPropertyChanged"/>
    /// <seealso cref="ModelCollectionBase&lt;T&gt;"/>
    public abstract class ModelBase : INotifyPropertyChanged {

        #region INotifyPropertyChanged

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        /// <seealso cref="INotifyPropertyChanged"/>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion INotifyPropertyChanged

        #region Public methods

        /// <summary>
        /// Obtain property name from property that is identified by a lambda expression.
        /// </summary>
        /// <typeparam name="T">Type that contains the property.</typeparam>
        /// <param name="propertyExpression">
        /// Lambda expression that identifies the property in a type-safe way:<br/>
        /// <code>GetPropertyName&lt;TModel&gt;(model =&gt; model.Property)</code>.
        /// </param>
        /// <returns>The property name as a <see cref="String"/>.</returns>
        /// <remarks>TODO: consider moving to a utility class.</remarks>
        /// <exception cref="ArgumentNullException">
        /// Thrown when the property expression is null.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// Thrown when the property expression does not identify a member. Also thrown when the
        /// given member is not a property.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// Thrown when the given member is not a property.
        /// </exception>
        [SuppressMessage(
            "Microsoft.Design",
            "CA1006:DoNotNestGenericTypesInMemberSignatures",
            Justification = "Easiest way to obtain a property name from a property."
        )]
        [SuppressMessage(
            "Microsoft.Design",
            "CA1011:ConsiderPassingBaseTypesAsParameters",
            Justification = "Easiest way to obtain a property name from a property.Also" + 
            "this will be redesigned with the migration to .NET 4.5"
        )]
        public static string GetPropertyName<T>(Expression<Func<T, object>> propertyExpression) {
            if (propertyExpression == null) {
                throw new ArgumentNullException("propertyExpression");
            }
            MemberExpression member = null;
            switch (propertyExpression.Body.NodeType) {
                case ExpressionType.MemberAccess:
                    member = propertyExpression.Body as MemberExpression;
                    break;

                case ExpressionType.Convert:
                    // Simple type property getter expressions are wrapped in a unary 
                    // conversion to Object.
                    var unaryExpression = propertyExpression.Body as UnaryExpression;
                    if (unaryExpression != null) {
                        member = unaryExpression.Operand as MemberExpression;
                    }
                    break;

                default:
                    // Do nothing.
                    break;
            }
            if (member == null) {
                throw new InvalidOperationException(
                    "Expression: " + propertyExpression.Body + " does not identify a member."
                );
            }
            var memberName = member.Member.Name;
            if (
                (member.Expression != propertyExpression.Parameters[0]) ||
                (member.Member.MemberType != MemberTypes.Property)
            ) {
                throw new InvalidOperationException(
                    "The member " + memberName + " is not a property."
                );
            }
            return memberName;
        }

        #endregion

        #region Protected methods (Code duplicate of ModelCollectionBase)

        /// <summary>
        /// Notify changes to properties on this model. 
        /// </summary>
        /// <param name="propertyName">
        /// String that is equal to the name of the property that changed.
        /// </param>
        /// <remarks>
        /// This method provides a direct way to fire property change notifications. 
        /// The alternative with the property expression is preferred because it is type 
        /// safe and resilient to refactoring operations.
        /// </remarks>
        protected void NotifyPropertyChanged(string propertyName) {
            // TODO: do we really want to expose this method?
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// Notify changes to properties on this model. 
        /// </summary>
        /// <typeparam name="TModel">Type of the derived model.</typeparam>
        /// <param name="propertyExpression">
        /// Lambda expression that identifies the property in a type-safe way.
        /// </param>
        /// <remarks>
        /// This method provides a safer way to identify the property that was changed.<br/>
        /// Identification by name is not safe because you have to remember to update the 
        /// notification call when the property name is changed.<br/>
        /// Now you can use a lambda expression to identify the property that changed.<br/>
        /// Example:<br/>
        /// <code>
        /// NotifyPropertyChanged&lt;ModelType, float>(p => p.ZoomParameter);
        /// </code>
        /// Adding the following method to your model implementation makes the notification
        /// even more simple.<br/>
        /// Method:<br/>
        /// <code>
        /// private void NotifyPropertyChanged&lt;T>(
        ///     Expression&lt;Func&lt;ModelType, T>> property
        /// ) {
        ///     NotifyPropertyChanged&lt;ModelType, T>(property);
        /// }
        /// </code>
        /// Notification call:<br/>
        /// <code>
        /// NotifyPropertyChanged(p => p.MyProperty);
        /// </code>
        /// </remarks>
        /// <exception cref="InvalidOperationException">
        /// Thrown when this type cannot notify property changes for the given type. 
        /// </exception>
        [SuppressMessage(
            "Microsoft.Design",
            "CA1006:DoNotNestGenericTypesInMemberSignatures",
            Justification = "Easiest way to notify that a property has changed on model."
        )]
        protected void NotifyPropertyChanged<TModel>(
            Expression<Func<TModel, object>> propertyExpression
        ) {
            if (!(this is TModel)) {
                throw new InvalidOperationException(
                    string.Format(
                        CultureInfo.InvariantCulture,
                        "Type '{0}' cannot notify property changes for type {1}.",
                        GetType().Name,
                        typeof(TModel).Name
                        )
                    );
            }
            var propertyName = GetPropertyName(propertyExpression);
            NotifyPropertyChanged(propertyName);
        }

        #endregion Protected methods (Code duplicate of ModelCollectionBase)

    }

}
